/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import io.sentry.protocol.Contexts;
import io.sentry.protocol.Request;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryTransaction;
import io.sentry.util.Objects;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * SentryTracer
 *
 * @since 2021-06-02
 **/
@ApiStatus.Internal
public final class SentryTracer implements ITransaction {
    private final @NotNull SentryId eventId = new SentryId();
    private final @NotNull Span root;
    private final @NotNull List<Span> children = new CopyOnWriteArrayList<>();
    private final @NotNull IHub hub;
    private final @NotNull Contexts contexts = new Contexts();
    private @Nullable Request request;
    private @NotNull String name;

    /**
     * constructor
     *
     * @param context TransactionContext
     * @param hub IHub
     */
    public SentryTracer(final @NotNull TransactionContext context, final @NotNull IHub hub) {
        Objects.requireNonNull(context, "context is required");
        Objects.requireNonNull(hub, "hub is required");
        this.root = new Span(context, this, hub);
        this.name = context.getName();
        this.hub = hub;
    }

    /**
     * getChildren
     *
     * @return List<Span>
     */
    public @NotNull List<Span> getChildren() {
        return children;
    }

    /**
     * getStartTimestamp
     *
     * @return Date
     */
    public @NotNull Date getStartTimestamp() {
        return this.root.getStartTimestamp();
    }

    /**
     * getTimestamp
     *
     * @return Date
     */
    public @Nullable Date getTimestamp() {
        return this.root.getTimestamp();
    }

    /**
     * Starts a child Span with given trace id and parent span id.
     *
     * @param parentSpanId - parent span id
     * @param operation - span operation name
     * @param description - span description
     * @return a new transaction span
     */
    @NotNull
    ISpan startChild(
        final @NotNull SpanId parentSpanId,
        final @NotNull String operation,
        final @Nullable String description) {
        final ISpan span = startChild(parentSpanId, operation);
        span.setDescription(description);
        return span;
    }

    /**
     * Starts a child Span with given trace id and parent span id.
     *
     * @param parentSpanId - parent span id
     * @param operation operation
     * @return a new transaction span
     */
    @NotNull
    private ISpan startChild(final @NotNull SpanId parentSpanId, final @NotNull String operation) {
        Objects.requireNonNull(parentSpanId, "parentSpanId is required");
        Objects.requireNonNull(operation, "operation is required");
        final Span span = new Span(root.getTraceId(), parentSpanId, this, operation, this.hub);
        this.children.add(span);
        return span;
    }

    @Override
    public @NotNull ISpan startChild(final @NotNull String operation) {
        return this.startChild(operation, null);
    }

    @Override
    public @NotNull ISpan startChild(
        final @NotNull String operation, final @Nullable String description) {
        if (children.size() < hub.getOptions().getMaxSpans()) {
            return root.startChild(operation, description);
        } else {
            hub.getOptions()
                .getLogger()
                .log(
                    SentryLevel.WARNING,
                    "Span operation: %s, description: %s dropped due to limit reached. Returning NoOpSpan.",
                    operation,
                    description);
            return NoOpSpan.getInstance();
        }
    }

    @Override
    public @NotNull SentryTraceHeader toSentryTrace() {
        return root.toSentryTrace();
    }

    @Override
    public void finish() {
        this.finish(this.getStatus());
    }

    @Override
    public void finish(@Nullable SpanStatus status) {
        if (!root.isFinished()) {
            root.finish(status);
            hub.configureScope(
                scope -> {
                    scope.withTransaction(
                        transaction -> {
                            if (transaction == this) {
                                scope.clearTransaction();
                            }
                        });
                });
            SentryTransaction transaction = new SentryTransaction(this);
            hub.captureTransaction(transaction);
        }
    }

    @Override
    public void setOperation(final @NotNull String operation) {
        this.root.setOperation(operation);
    }

    @Override
    public @NotNull String getOperation() {
        return this.root.getOperation();
    }

    @Override
    public void setDescription(final @Nullable String description) {
        this.root.setDescription(description);
    }

    @Override
    public @Nullable String getDescription() {
        return this.root.getDescription();
    }

    @Override
    public void setStatus(final @Nullable SpanStatus status) {
        this.root.setStatus(status);
    }

    @Override
    public @Nullable SpanStatus getStatus() {
        return this.root.getStatus();
    }

    @Override
    public void setThrowable(final @Nullable Throwable throwable) {
        this.root.setThrowable(throwable);
    }

    @Override
    public @Nullable Throwable getThrowable() {
        return this.root.getThrowable();
    }

    @Override
    public @NotNull SpanContext getSpanContext() {
        return this.root.getSpanContext();
    }

    @Override
    public void setTag(final @NotNull String key, final @NotNull String value) {
        this.root.setTag(key, value);
    }

    @Override
    public @Nullable String getTag(final @NotNull String key) {
        return this.root.getTag(key);
    }

    @Override
    public boolean isFinished() {
        return this.root.isFinished();
    }

    @Override
    public @Nullable Boolean isSampled() {
        return this.root.isSampled();
    }

    @Override
    public void setName(@NotNull String name) {
        this.name = name;
    }

    @Override
    public @NotNull String getName() {
        return this.name;
    }

    @Override
    @Deprecated
    @ApiStatus.ScheduledForRemoval
    public void setRequest(final @Nullable Request request) {
        this.request = request;
    }

    @Override
    @Deprecated
    @ApiStatus.ScheduledForRemoval
    public @Nullable Request getRequest() {
        return this.request;
    }

    @Override
    @Deprecated
    @ApiStatus.ScheduledForRemoval
    public @NotNull Contexts getContexts() {
        return this.contexts;
    }

    @Override
    public @NotNull List<Span> getSpans() {
        return this.children;
    }

    @Override
    public @Nullable Span getLatestActiveSpan() {
        final List<Span> spans = new ArrayList<>(this.children);
        if (!spans.isEmpty()) {
            for (int i = spans.size() - 1; i >= 0; i--) {
                if (!spans.get(i).isFinished()) {
                    return spans.get(i);
                }
            }
        }
        return null;
    }

    @Override
    public @NotNull SentryId getEventId() {
        return eventId;
    }

    @NotNull
    Span getRoot() {
        return root;
    }
}
